import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.*;
import java.util.Observable;
import java.util.Observer;
import java.util.Scanner;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;

/**
 * Responsible for holding the GUI displayed to the user. 
 * Also contains a selectable menu from which load/saves and hints may be accessed.
 * Acts as the primary interactive interface for the user.
 */
public class SudokuFrontEnd extends JFrame implements ComponentListener, ActionListener, Printable, WindowListener, MouseListener
{
	// local variables
	// game settings
	private Difficulty difficulty;
	private boolean gameModified = true;
	
	// local objects
	private GridBagConstraints gridBagConstraints;
	private GridControl gridControl;
	private JLabel labelTitle;
	private SudokuBackEnd sudokuBackEnd;
	GridInterface gridInterface;
	private Observer numberEntered = new Observer()
	{
			@Override
			public void update(Observable o, Object arg)
			{
				if (arg instanceof Point)
				{
					gameModified = true;
				}
			}
		};
		
	private JMenuBar menuBar;
	private JMenu fileMenu;
	private JMenuItem newGameItem;
	private JMenuItem loadGameItem;
	private JMenuItem saveGameItem;
	private JMenuItem exitItem;
	private JMenu gameMenu;
	private JMenuItem hintItem;
	private JMenuItem printItem;
	private JMenu helpMenu;
	private JMenuItem aboutItem;
	
	// Magic Numbers
	public static final int noSuperCellColumns = 3;
	public static final int noSuperCellRows = 3;
	public static final int noCellColumns = 3;
	public static final int noCellRows = 3;
	public static final int superCellCount = 9;
	public static final int cellCount = 81;
	public static final int rowCount = 9;
	public static final int colCount = 9;
	
	/**
	 * Initialises an interative Sudoku game grid based on a given title and selected difficulty
	 * @param title Title of the Sudoku
	 * @param difficulty selected difficulty setting
	 */
	public SudokuFrontEnd(String title, Difficulty difficulty)
	{
		super(title);
		gridControl = new GridControl();
		this.setLayout(new GridBagLayout());
		this.addWindowListener(this);
		this.addMouseListener(this);
		gridInterface = gridControl;
		this.difficulty = difficulty;
		initialiseAndDrawWindow();
		this.getContentPane().setBackground(Color.white);
		
		this.setLocation((Toolkit.getDefaultToolkit().getScreenSize().width - getWidth()) / 2, (Toolkit.getDefaultToolkit().getScreenSize().height - getHeight()) / 2);
		this.setVisible(true);
	}
	
	/**
	 * Draws the interactive interface on which the grid is located.
	 * Also draws the selectable menu items
	 * Also passes parameters to the back end to process such as the generation of a solution grid
	 */
	private void initialiseAndDrawWindow()
	{
		this.addComponentListener(this);
		gridBagConstraints = new GridBagConstraints();
		gridBagConstraints.fill = GridBagConstraints.BOTH;
		setBackground(Color.white);
		
		
		gridBagConstraints.weightx = 1.0;
		gridBagConstraints.weighty = 1.0;
		gridBagConstraints.gridx = 1;
		gridBagConstraints.gridy = 3;
		
		this.add(gridControl, gridBagConstraints);
		
		labelTitle = new JLabel("Sudoku");
		labelTitle.setFont(new Font("Segoe UI", Font.PLAIN, 35));
		gridBagConstraints.fill = GridBagConstraints.NONE;
		gridBagConstraints.anchor = GridBagConstraints.CENTER;
		gridBagConstraints.weightx = 0.5;
		gridBagConstraints.weighty = 0.05;
		gridBagConstraints.gridx = 1;
		gridBagConstraints.gridy = 0;
		
		this.add(labelTitle, gridBagConstraints);
		
		
		/*
		 * File -> New Game
		 * 		-> Load Game...
		 * 		-> Exit
		 * Game -> Hint
		 * 		-> Leaderboard
		 * Help -> About...
		 * 
		 * */
		
		menuBar = new JMenuBar();
		fileMenu = new JMenu("File");
		newGameItem = new JMenuItem("New Game");
		loadGameItem = new JMenuItem("Load Game...");
		saveGameItem = new JMenuItem("Save Game...");
		exitItem = new JMenuItem("Exit");
		gameMenu = new JMenu("Game");
		hintItem = new JMenuItem("Hint");
		printItem = new JMenuItem("Print...");
		helpMenu = new JMenu("About");
		aboutItem = new JMenuItem("About");
		
		newGameItem.addActionListener(this);
		loadGameItem.addActionListener(this);
		saveGameItem.addActionListener(this);
		exitItem.addActionListener(this);
		hintItem.addActionListener(this);
		printItem.addActionListener(this);
		aboutItem.addActionListener(this);
		
		newGameItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, Event.CTRL_MASK));
		loadGameItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Event.CTRL_MASK));
		saveGameItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Event.CTRL_MASK));
		hintItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, Event.CTRL_MASK));
		exitItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, Event.CTRL_MASK));
		printItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, Event.CTRL_MASK));
		aboutItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, Event.CTRL_MASK));
		
		menuBar.add(fileMenu);
		menuBar.add(gameMenu);
		menuBar.add(helpMenu);
		fileMenu.add(newGameItem);
		fileMenu.add(loadGameItem);
		fileMenu.add(saveGameItem);
		fileMenu.add(exitItem);
		gameMenu.add(hintItem);
		gameMenu.add(printItem);
		helpMenu.add(aboutItem);

		this.setJMenuBar(menuBar);
		sudokuBackEnd = new SudokuBackEnd(gridInterface);
		sudokuBackEnd.generateBoard(difficulty);
		gridInterface.addNumberEnteredObserver(sudokuBackEnd.numberEntered);
		gridInterface.addNumberEnteredObserver(numberEntered);
		this.pack();
		this.setSize(700, 700);
	}
	
	
	
	/**
	 * Clears the current board and creates a new game based on current difficulty level
	 */
	public void newGame()
	{
		if (gameModified)
		{
			int result = JOptionPane.showOptionDialog(null, "This game has been modified, would you like to save the game?", "Confirm Quit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null);
			if (result == JOptionPane.YES_OPTION)
			{
				saveGame();
			}
		}
		clearBoard();
		sudokuBackEnd.generateBoard(difficulty);
	}
	
	/**
	 * Creates a new instance of the Sudoku game
	 */
	public void clearBoard()
	{
		gridInterface.resetControl();
		sudokuBackEnd = new SudokuBackEnd(gridInterface);
		gridInterface.addNumberEnteredObserver(sudokuBackEnd.numberEntered);
	}
	
	/**
	 * Allows the user to load a saved Sudoku game. Files must be of the type .sudoku.
	 * Additionally checks for file validity when filling out the grid with saved cell values.
	 * Cells are filled based on the grid saved, and the locked/visible values to the user
	 */
	public void loadGame()
	{
		boolean loop = true;
		int result;
		JFileChooser openDialog = new JFileChooser();
		openDialog.setMultiSelectionEnabled(false);
		openDialog.setAcceptAllFileFilterUsed(false);
		FileNameExtensionFilter filter = new FileNameExtensionFilter("Sudoku Game Save Files (.sudoku)", "sudoku");
		openDialog.setFileFilter(filter);
		try
		{
			while (loop)
			{
				result = openDialog.showDialog(this, "Load Game");
				if (result == JFileChooser.APPROVE_OPTION)
				{
					File loadFile = new File(openDialog.getSelectedFile().getAbsolutePath());
					
						Scanner sc = new Scanner(loadFile);
						boolean parseInput = true;
						boolean[][] givenCells = new boolean[colCount][rowCount];
						int[][] allFilledCells = new int[colCount][rowCount];
						loop = false;
						for (int i = 0; i < cellCount && parseInput == true; i++)
						{
							if (sc.hasNextBoolean())
							{
								boolean j = sc.nextBoolean();
								givenCells[i % colCount][i / rowCount] = j;
							}
							else
							{
								JOptionPane.showMessageDialog(null, "Invalid File");
								loop = true;
								parseInput = false;
							}
						}
						for (int i = 0; i < cellCount && parseInput == true; i++)
						{
							if (sc.hasNextInt())
							{
								int j = sc.nextInt();
								if (j < 10 && j > -1)
								{
									allFilledCells[i % colCount][i / rowCount] = j;
								}
								else
								{
									JOptionPane.showMessageDialog(null, "Invalid File");
									loop = true;
									parseInput = false;
								}
							}
							else
							{
								JOptionPane.showMessageDialog(null, "Invalid File");
								loop = true;
								parseInput = false;
							}
						}
						if (parseInput)
						{
							clearBoard();
							if (!sudokuBackEnd.loadGame(givenCells, allFilledCells))
							{
								JOptionPane.showMessageDialog(null, "Invalid File");
								loop = true;
							}
						}
						sc.close();
					}
				else
				{
					loop = false;
				}
			}
		}
		catch (FileNotFoundException e)
		{
			JOptionPane.showMessageDialog(null, "Invalid File");
			loop = true;
		}
	}
	
	/**
	 * Attempts to save a copy of the current game state.
	 * Saves a copy of the locked cells, i.e. those given to the user, and the currently filled cells by user.
	 * Save format as .sudoku, overwrite function included if existing file shares chosen name.
	 */
	public void saveGame()
	{
		int[][] cellsData = gridInterface.getCellsData();
		boolean[][] givenCells = new boolean[colCount][rowCount];
		FileWriter fw = null;
		JFileChooser saveDialog = new JFileChooser();
		saveDialog.setAcceptAllFileFilterUsed(false);
		FileNameExtensionFilter filter = new FileNameExtensionFilter("Sudoku Game Save Files (.sudoku)", "sudoku");
		saveDialog.setFileFilter(filter);
		int result;
		boolean loop = true;
		boolean makeWrite = false;
		String fileName;
		File file = null;
		
		
		try
		{
			loop = true;
			while (loop)
			{
				result = saveDialog.showDialog(null, "Save Game");
				if (result == JFileChooser.APPROVE_OPTION)
				{
					fileName = saveDialog.getSelectedFile().getAbsolutePath();
					if (!saveDialog.getSelectedFile().getAbsolutePath().endsWith(filter.getExtensions()[0]))
					{
						fileName += "." + filter.getExtensions()[0];
					}
					file = new File(fileName);
					int resultOverwrite;
					if (file.exists())
					{
						resultOverwrite = JOptionPane.showOptionDialog(null, "Do you want to overwrite this file? (" + file.getName() + ")", "Overwrite?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null);
						if (resultOverwrite == JOptionPane.YES_OPTION)
						{
							makeWrite = true;
							loop = false;
						}
					}
					else
					{
						makeWrite = true;
						loop = false;
					}
				}
				else
				{
					loop = false;
				}
			}
			if (makeWrite)
			{
				fw = new FileWriter(file);
				
				for (int i = 0; i < cellCount; i++)
				{
					Point p = new Point(i / colCount, i % rowCount);
					if (gridInterface.getCell(p).isLocked())
					{
						givenCells[i % rowCount][i / colCount] = true;
						fw.write("true ");
					}
					else
					{
						fw.write("false ");
					}
				}
				for (int i = 0; i < 81; i++)
				{
					fw.write(cellsData[i / colCount][i % rowCount] + " ");
				}
				fw.close();
			}
			
		}
		catch (IOException e)
		{
			
		}
		gameModified = false;
	}
	
	/**
	 * Quits the game, tests if any modifications have occurred since last open.
	 * Prompts save message if modified.
	 * @return exit status
	 */
	public int exit()
	{
		if (gameModified)
		{
			int result = JOptionPane.showOptionDialog(null, "This game has been modified, would you like to save the game?", "Confirm Quit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null);
			if (result == JOptionPane.YES_OPTION)
			{
				saveGame();
			}
		}
		System.exit(0);
		return 0;
	}
	
	/**
	 * Calls for the generation of a hint
	 */
	public void hint()
	{
		sudokuBackEnd.giveHint();
	}
	
	/**
	 * Printer call
	 */
	public void print()
	{
		PrinterJob sudokuJob = PrinterJob.getPrinterJob();
		sudokuJob.setPrintable(this);
		boolean print = sudokuJob.printDialog();
		if (print)
		{
			try
			{
				sudokuJob.print();
			}
			catch (PrinterException e)
			{
				
			}
		}
	}
	
	/**
	 * Displays information about creators
	 */
	public void about()
	{
		String about = "Sudoku by John Wei-Hao Wang, Nathan Orner, and Ryan Woo.";
		JOptionPane.showMessageDialog(null, about, "About", JOptionPane.INFORMATION_MESSAGE);
	}

	@Override
	public void componentHidden(ComponentEvent e)
	{
	}

	@Override
	public void componentMoved(ComponentEvent e)
	{		
	}
	
	public void componentResized(ComponentEvent e)
	{
		if (gridInterface != null)
		{
			gridInterface.componentResized();
		}
	}

	@Override
	public void componentShown(ComponentEvent e)
	{	
	}

	/* 
	 * Translates selected option into their respective method calls.
	 */
	@Override
	public void actionPerformed(ActionEvent e) {
		if (e.getSource() instanceof JMenuItem)
		{
			JMenuItem menuItem = (JMenuItem)e.getSource();
			if (menuItem.equals(newGameItem))
			{
				newGame();
			}
			if (menuItem.equals(loadGameItem))
			{
				loadGame();
			}
			if (menuItem.equals(saveGameItem))
			{
				saveGame();
			}
			if (menuItem.equals(exitItem))
			{
				exit();
			}
			if (menuItem.equals(hintItem))
			{
				hint();
			}
			if (menuItem.equals(printItem))
			{
				print();
			}
			if (menuItem.equals(aboutItem))
			{
				about();
			}
		}
		
	}

	@Override
	public int print(Graphics graphics, PageFormat pageFormat, int pageIndex)
			throws PrinterException
	{
		Point dim = new Point();
		Graphics2D g = (Graphics2D)this.getGraphics();
		if (pageIndex > 0)
		{
			return NO_SUCH_PAGE;
		}
		
		gridInterface.grabImage(graphics, new Point((int)pageFormat.getImageableX(), (int)pageFormat.getImageableY()), new Point((int)pageFormat.getImageableWidth(), (int)pageFormat.getImageableHeight()));
		return PAGE_EXISTS;
	}

	@Override
	public void windowActivated(WindowEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void windowClosed(WindowEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void windowClosing(WindowEvent arg0) {
		if (gameModified)
		{
			int result = JOptionPane.showOptionDialog(null, "This game has been modified, would you like to save the game?", "Confirm Quit", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null);
			if (result == JOptionPane.YES_OPTION)
			{
				saveGame();
			}
		}
	}

	@Override
	public void windowDeactivated(WindowEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void windowDeiconified(WindowEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void windowIconified(WindowEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void windowOpened(WindowEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void mouseClicked(MouseEvent arg0) {
	}

	@Override
	public void mouseEntered(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void mouseExited(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void mousePressed(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void mouseReleased(MouseEvent arg0) {
		// TODO Auto-generated method stub
		
	}
}